﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using BEELab.Data;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Text;


namespace BEELab.Logic.ProgramManagement.VariableManagement
{
    public static class VariableManagement
    {
        public class Variable
        {

            /// <summary>
            /// Constructor - gets variable values from DB by given variableID, creates new variable
            /// with the same values and updates it's sessionID and SubjectID into new given values. 
            /// </summary>
            /// <param name="ID">Variable ID</param>
            /// <param name="SessionID">New Session ID</param>
            /// <param name="SubjectID">New Subject ID</param>
            public Variable(int ID, int SessionID, string SubjectID)
            {
                SqlCommand command = new SqlCommand();
                command.CommandText = "SELECT * FROM Variables WHERE ID=@ID";
                command.Parameters.Add(new SqlParameter("@ID", ID));
                DataTable dt = DBHelper.ExecuteQuery(command);

                //If found variable with variableID - place in new variable same values
                if (dt.Rows.Count == 1)
                {
                    DataRow dr = dt.Rows[0];
                    this.id = ID;
                    this.name = dr["Name"].ToString();
                    this.description = dr["Description"].ToString();
                    this.programID = int.Parse(dr["ProgramID"].ToString());
                    this.sessionID = SessionID;
                    this.subjectID = SubjectID;
                    this.scope = dr["Scope"].ToString();
                    this.islocked = false;

                    //If this variable value was changed in this specific invitation - get its value on this invitation
                    if (this.scope == "Subject" && SubjectID != null)
                    {
                        int InvitationID = SessionManagement.SessionManagement.GetInvitationIDBySubjectID(this.subjectID);
                        this.value = SessionManagement.InvitationManagement.GetVariableValue(InvitationID, this.id);
                    }
                    //If not - get its value from session and insert
                    else
                    {
                        this.value = SessionManagement.SessionManagement.GetVariableValue(this.sessionID, this.id);
                    }
                }
            }

            /// <summary>
            /// Constructor
            /// </summary>
            /// <param name="Name">Variable Name</param>
            /// <param name="Description">Variable Description</param>
            /// <param name="Scope">Variable Scope</param>
            /// <param name="Value">Variable Value</param>
            public Variable(string Name, string Description, string Scope, string Value)
            {
                this.name = Name;
                this.description = Description;
                this.scope = Scope;
                this.value = Value;

                this.sessionID = 0;
                this.subjectID = null;
                this.programID = 0;
            }

            private int sessionID;
            public int SessionID
            {
                set { sessionID = value; }
                get { return sessionID; }
            }

            private string subjectID;
            public string SubjectID
            {
                set { subjectID = value; }
                get { return subjectID; }
            }

            private int id;
            public int ID
            {
                get { return id; }
            }

            private int programID;
            public int ProgramID
            {
                get { return programID; }
            }

            private string name;
            public string Name
            {
                get { return name; }
            }

            private string description;
            public string Description
            {
                get { return description; }
            }

            private string scope;
            public string Scope
            {
                get { return scope; }
            }

            private string value;
            public string Value
            {
                set {
                    this.value = value;
                    if (this.scope == "Session")
                    {
                        Logic.Execution.SessionRecovery.SaveSessionVariable(this.sessionID, this.name, this.value);
                    }
                    else
                    {
                        Logic.Execution.SessionRecovery.SaveSubjectVariable(this.sessionID, this.subjectID, this.name, this.value);
                    }
                }
                get {
                    return value;
                }
            }

            public Mutex mutex = new Mutex(false);
            private Queue<KeyValuePair<string, string>> queue = new Queue<KeyValuePair<string, string>>();
            public string lockOwner = null;
            private DateTime lockTime;

            /// <summary>
            /// Handles lock management
            /// </summary>
            /// <param name="SubjectID">Subject ID that requested the lock</param>
            /// <param name="callback">callback</param>
            /// <returns>True of lock succeeded, False otherwise</returns>
            public Boolean Lock(string SubjectID, string callback)
            {
                Boolean res = false;

                mutex.WaitOne();

                //If some user holds the lock
                if (islocked)
                {
                    //If current user holds the lock
                    if (lockOwner != null && lockOwner.Equals(SubjectID))
                    {
                        res = true;

                        lockTime = System.DateTime.Now;
                        TimerCallback tcb = this.CheckLock;
                        System.Threading.Timer timer = new System.Threading.Timer(tcb, this, 60000, System.Threading.Timeout.Infinite);

                        BEELab.Logic.Execution.MessageHub.NotifyGotLock(SubjectID, callback);
                    }
                    //If other user holds the lock
                    else
                    {
                        res = false;

                        bool found = false;
                        //Search current user in the lock's queue
                        foreach (KeyValuePair<string, string> item in queue)
                        {
                            if (item.Key == SubjectID)
                            {
                                found = true;
                            }
                        }
                        //If user is not in the queue - add it to the end of queue
                        if (!found)
                        {
                            queue.Enqueue(new KeyValuePair<string, string>(SubjectID, callback));
                        }
                    }
                }
                //Lock is free - give it to the current user
                else
                {
                    lockOwner = SubjectID;
                    islocked = true;
                    res = true;
                    BEELab.Logic.Execution.MessageHub.NotifyGotLock(SubjectID, callback);

                    lockTime = System.DateTime.Now;
                    TimerCallback tcb = this.CheckLock;
                    System.Threading.Timer timer = new System.Threading.Timer(tcb, this, 60000, System.Threading.Timeout.Infinite);
                }
                mutex.ReleaseMutex();

                return res;
            }

            /// <summary>
            /// Checks timeout of the specific object - if the same user holds the object for more
            /// than 60 seconds, release it from user (but keep mutex locked).
            /// If the object is not locked - release its mutex free.
            /// </summary>
            /// <param name="obj">object to check</param>
            public void CheckLock(Object obj)
            {
                Variable var = (Variable)obj;
                DateTime now = System.DateTime.Now;

                var.mutex.WaitOne();
                if (var.isLocked && now.Subtract(var.lockTime).Seconds >= 60)
                {
                    var.Release(var.lockOwner);
                }
                else
                {
                    var.mutex.ReleaseMutex();
                }
            }

            /// <summary>
            /// Function tries to lock mutex by given subjectID.
            /// </summary>
            /// <param name="SubjectID">Subject ID</param>
            /// <returns>True if succeeded, False otherwise</returns>
            public Boolean Lock(string SubjectID)
            {
                Boolean res = false;

                mutex.WaitOne();
                //If mutex locked
                if (islocked)
                {
                    //If current user holds the mutex return true
                    if (lockOwner != null && lockOwner.Equals(SubjectID))
                    {
                        res = true;
                    }
                    //if other user hold it return false
                    else
                    {
                        res = false;
                    }
                }
                //if mutex is free- lock it by current user and return true
                else
                {
                    lockOwner = SubjectID;
                    islocked = true;
                    res = true;
                }
                mutex.ReleaseMutex();

                return res;
            }

            /// <summary>
            /// Releases mutex that locked by sujectID and give it to next waiting subject, or release ut
            /// if queue is empty
            /// </summary>
            /// <param name="SubjectID">Subject ID</param>
            /// <returns>True if lock released from current user, False otherwise</returns>
            public Boolean Release(string SubjectID)
            {
                Boolean res;

                mutex.WaitOne();
                //If current user holds the lock
                if (islocked && lockOwner.Equals(SubjectID))
                {
                    //If there is no one in lock's queue - release it
                    if (queue.Count == 0)
                    {
                        this.islocked = false;
                        this.lockOwner = null;
                        res = true;
                    }
                    //If queue is full, give lock to next user
                    else
                    {
                        //Get next user in line
                        KeyValuePair<string, string> kvp = queue.Dequeue();
                        this.lockOwner = kvp.Key;
                        BEELab.Logic.Execution.MessageHub.NotifyGotLock(this.lockOwner, kvp.Value);
                        res = true;

                        //Update lock time of mutex
                        lockTime = System.DateTime.Now;
                        TimerCallback tcb = this.CheckLock;
                        System.Threading.Timer timer = new System.Threading.Timer(tcb, this, 60000, System.Threading.Timeout.Infinite);
                    }
                }
                else
                {
                    res = false;
                }
                mutex.ReleaseMutex();

                return res;
            }

            private Boolean islocked;
            public Boolean isLocked
            {
                get { return islocked; }
            }
        }

        /// <summary>
        /// Creates new variable, with values as same as an existing variable with the same type,
        /// except ProgramID number and Name. Insert variable to DB.
        /// </summary>
        /// <param name="VariableID">VariableID</param>
        /// <param name="ProgramID">ProgramID</param>
        /// <param name="NamePrefix">NamePrefix</param>
        /// <returns>Number of rows effected on DB</returns>
        public static int ImportVariable(int VariableID, int ProgramID, string NamePrefix)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "SELECT Name, Description, DefaultValue, Scope FROM Variables WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@ID", VariableID));
            DataTable dt = DBHelper.ExecuteQuery(command);

            if (dt.Rows.Count != 1)
                return -1;

            DataRow dr = dt.Rows[0];

            return CreateVariable(ProgramID, NamePrefix + dr["Name"].ToString(), dr["Description"].ToString(), dr["DefaultValue"].ToString(), dr["Scope"].ToString());
        }


        /// <summary>
        /// Creates new Variavle and insert it to DB.
        /// </summary>
        /// <param name="ProgramID">Program ID</param>
        /// <param name="Name">Variavle Name</param>
        /// <param name="Description">Variavle Description</param>
        /// <param name="DefaultValue">Variavle Default Value</param>
        /// <param name="Scope">Scope (session or subject)</param>
        /// <returns>Number of rows effected on DB</returns>
        public static int CreateVariable(int ProgramID, string Name, string Description, string DefaultValue, string Scope)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "INSERT INTO Variables (ProgramID, Name, Description, DefaultValue, Scope) VALUES (@ProgramID, @Name, @Description, @DefaultValue, @Scope); SELECT SCOPE_IDENTITY();";
            command.Parameters.Add(new SqlParameter("@ProgramID", ProgramID));
            command.Parameters.Add(new SqlParameter("@Name", Name));
            command.Parameters.Add(new SqlParameter("@Description", Description));
            command.Parameters.Add(new SqlParameter("@DefaultValue", DefaultValue));
            command.Parameters.Add(new SqlParameter("@Scope", Scope));

            return int.Parse(DBHelper.ExecuteScalar(command));
        }

        /// <summary>
        /// Deleted Variavle from DB
        /// </summary>
        /// <param name="ID">Variavle ID</param>
        /// <returns>True if deleted, False otherwise</returns>
        public static Boolean DeleteVariable(int ID)
        {
            ProgramManagement.StageManagement.ContainerManagement.RemoveElement("Variable", ID);

            SqlCommand command = new SqlCommand();
            command.CommandText = "DELETE FROM Variables WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@ID", ID));

            int res = DBHelper.ExecuteNonQuery(command);

            if (res > 0)
                return true;

            return false;
        }

        /// <summary>
        /// Updates Variavle value on DB
        /// </summary>
        /// <param name="Name">Variavle Name</param>
        /// <param name="Description">Variavle Description</param>
        /// <param name="DefaultValue">Variavle Default Value</param>
        /// <param name="Scope">Scope (session or subject)</param>
        /// <returns>True if variavle values updated, False otherwise</returns>
        public static Boolean UpdateVariable(int ID, string Name, string Description, string DefaultValue, string Scope)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "UPDATE Variables SET Name=@Name, Description=@Description, DefaultValue=@DefaultValue, Scope=@Scope WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@Name", Name));
            command.Parameters.Add(new SqlParameter("@Description", Description));
            command.Parameters.Add(new SqlParameter("@DefaultValue", DefaultValue));
            command.Parameters.Add(new SqlParameter("@Scope", Scope));
            command.Parameters.Add(new SqlParameter("@ID", ID));

            int res = DBHelper.ExecuteNonQuery(command);

            if (res == 1)
                return true;

            return false;
        }

        /// <summary>
        /// Gets all existing variables IDs of a specific Experiment
        /// </summary>
        /// <param name="ID">Program ID</param>
        /// <returns>All existing variables IDs in a list</returns>
        public static List<int> ListByProgramID(int ID)
        {
            List<int> result = new List<int>();

            SqlCommand command = new SqlCommand();
            command.CommandText = "SELECT ID FROM Variables WHERE ProgramID=@ProgramID";
            command.Parameters.Add(new SqlParameter("@ProgramID", ID));
            DataTable dt = DBHelper.ExecuteQuery(command);

            foreach (DataRow dr in dt.Rows)
            {
                result.Add((int)dr["ID"]);
            }

            return result;
        }

        /// <summary>
        /// Gets variable default value from DB
        /// </summary>
        /// <param name="ID">Variable ID</param>
        /// <returns>Variable default value</returns>
        public static string GetDefaultValue(int ID)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "SELECT DefaultValue FROM Variables WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@ID", ID));
            return DBHelper.ExecuteScalar(command);
        }

        /// <summary>
        /// Sets variable default value on DB
        /// </summary>
        /// <param name="ID">Variable ID</param>
        /// <param name="DefaultValue">New Default Value</param>
        /// <returns>True if default updated, False otherwise</returns>
        public static Boolean SetDefaultValue(int ID, string DefaultValue)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "UPDATE Variables SET DefaultValue=@DefaultValue WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@DefaultValue", DefaultValue));
            command.Parameters.Add(new SqlParameter("@ID", ID));

            int res = DBHelper.ExecuteNonQuery(command);

            if (res == 1)
                return true;

            return false;
        }

        /// <summary>
        /// Gets an Experiment (Program) ID and and variable name, and returns variable's ID number
        /// </summary>
        /// <param name="ProgramID">Experiment/Program ID number</param>
        /// <param name="Name">Variable Name</param>
        /// <returns>Variable ID number on DB</returns>
        public static int GetIDByName(int ProgramID, string Name)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "SELECT ID FROM Variables WHERE ProgramID=@ProgramID AND Name=@Name";
            command.Parameters.Add(new SqlParameter("@ProgramID", ProgramID));
            command.Parameters.Add(new SqlParameter("@Name", Name));
            return int.Parse(DBHelper.ExecuteScalar(command));
        }

        /// <summary>
        /// Gets variable's name by its ID.
        /// </summary>
        /// <param name="ID">Variable ID</param>
        /// <returns>Variable Name</returns>
        public static string GetNameByID(int ID)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "SELECT Name FROM Variables WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@ID", ID));
            return DBHelper.ExecuteScalar(command);
        }

        /// <summary>
        /// Gets program ID according to Variable's ID
        /// </summary>
        /// <param name="ID">Variable ID</param>
        /// <returns>Program's ID</returns>
        public static int GetProgramID(int ID)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "Select ProgramID FROM Variables WHERE ID=@ID";
            command.Parameters.Add(new SqlParameter("@ID", ID));
            return int.Parse(DBHelper.ExecuteScalar(command));
        }

        /// <summary>
        /// Gets Variable ID and retreive its parent Experiment ID.
        /// According to Experiment ID returns string that contains ALL variables names exists on this experiment,
        /// seperated by given separator.
        /// </summary>
        /// <param name="ID">Variable ID</param>
        /// <param name="separator">String separator</param>
        /// <returns>String that contains all variables names of an experiment, seperated by given seperator</returns>
        public static string EnumerateVariables(int ID, string separator)
        {
            int ProgramID = GetProgramID(ID);
            string name = GetNameByID(ID);

            StringBuilder res = new StringBuilder();

            //Gets all variables names of a specific experiment and insert it into DataTable dt
            SqlCommand command = new SqlCommand();
            command.CommandText = "Select Name FROM Variables WHERE ProgramID=@ProgramID";
            command.Parameters.Add(new SqlParameter("@ProgramID", ProgramID));
            DataTable dt = DBHelper.ExecuteQuery(command);

            res.Append("SubjectID$|SubjectList$|SubjectCount$|NextStage$|CurrentStage$");

            foreach (DataRow row in dt.Rows)
            {
                if (name != row[0].ToString())
                {
                    res.Append(separator);                    
                    res.Append(row[0].ToString());
                    res.Append("$");
                }
            }

            return res.ToString();
        }


        /// <summary>
        /// Gets Experiment ID and returns string that contains ALL variables names exists on this experiment,
        /// seperated by given separator.
        /// </summary>
        /// <param name="ID">Experiment ID</param>
        /// <param name="separator">String separator</param>
        /// <returns>String that contains all variables names of an experiment, seperated by given seperator</returns>
        public static string EnumerateVariablesForProgram(int ProgramID, string separator)
        {
            StringBuilder res = new StringBuilder();

            SqlCommand command = new SqlCommand();
            command.CommandText = "Select Name FROM Variables WHERE ProgramID=@ProgramID";
            command.Parameters.Add(new SqlParameter("@ProgramID", ProgramID));
            DataTable dt = DBHelper.ExecuteQuery(command);

            res.Append("SubjectID$|SubjectList$|SubjectCount$|NextStage$|CurrentStage$");


            foreach (DataRow row in dt.Rows)
            {
                res.Append(separator);
                res.Append(row[0].ToString());
                res.Append("$");
            }

            return res.ToString();
        }

        /// <summary>
        /// Gets Variable ID and returns Owner ID of the user who own the experiment that this variable belongs to
        /// </summary>
        /// <param name="ID">Variable ID</param>
        /// <returns>Owner ID of the user who own the experiment, or 0 if variable not found</returns>
        public static int GetOwnerID(int ID)
        {
            SqlCommand command = new SqlCommand();
            command.CommandText = "SELECT Programs.OwnerID FROM Variables JOIN Programs ON Programs.ID=Variables.ProgramID WHERE Variables.ID=@ID";
            command.Parameters.Add(new SqlParameter("@ID", ID));
            string res = DBHelper.ExecuteScalar(command);

            if (res != null && res.Length > 0)
            {
                return int.Parse(res);
            }

            return 0;
        }
    }
}